package net.yters.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import net.yters.model.ModelState;
import net.yters.model.agents.DeciderAgent;
import net.yters.model.agents.concrete.ExternalInputAgent;
import cern.colt.function.ObjectFunction;
import cern.jet.random.Normal;

public class Util {

	@SuppressWarnings("rawtypes")
	public static void emboxArray(ArrayList boxes, ArrayList values) {
		for (int i = 0; i < boxes.size(); i++) {
			((Box) boxes.get(i)).f_value = values.get(i);
		}
	}

	public static ArrayList<Object> disemBoxArray(ArrayList<Box> boxes) {
		ArrayList<Object> values = new ArrayList<Object>();

		for (int i = 0; i < boxes.size(); i++) {
			values.add(boxes.get(i).f_value);
		}

		return values;
	}

	public static double[] disemBoxDoubleArray(ArrayList<Box> boxes) {
		double[] values = new double[boxes.size()];

		for (int i = 0; i < boxes.size(); i++) {
			values[i] = (Double) boxes.get(i).f_value;
		}

		return values;
	}

	public static ArrayList<Double> add(ArrayList<Object> a, ArrayList<Object> b) {
		ArrayList<Double> c = new ArrayList<Double>();

		for (int i = 0; i < a.size(); i++) {
			c.add(new Double(Math.round((Double) a.get(i) + (Double) b.get(i))));
		}

		return c;
	}

	public static ArrayList<Object> mult(ArrayList<Object> a, Double b) {
		ArrayList<Object> c = new ArrayList<Object>();

		for (int i = 0; i < a.size(); i++) {
			c.add(new Double(Math.round((Double) a.get(i) * b)));
		}

		return c;
	}

	@SuppressWarnings("rawtypes")
	public static ArrayList<Double> rotate(ArrayList a, Double b) { // TODO HACK
		// TODO make this easily extensible to 3D
		ArrayList<Double> c = new ArrayList<Double>();

		Double x = (Double) a.get(0);
		Double y = (Double) a.get(1);
		Double angle = Math.atan2(y, x) + b; // TODO HACK

		c.add(Math.cos(angle));
		c.add(Math.sin(angle));

		return c;
	}

	public static ArrayList<Double> closestAgentByType(DeciderAgent agent, ArrayList<DeciderAgent> agents, Double type) {
		ArrayList<Double> input = new ArrayList<Double>();

		DeciderAgent closestAgent = null;
		Double closestDistance = Double.POSITIVE_INFINITY;
		Double closestAngle = null;

		Double distance = Double.POSITIVE_INFINITY;

		Double x, y, x1, y1, x2, y2;

		for (DeciderAgent a : agents) {
			x1 = (Double) agent.f_position.get(0).f_value;
			y1 = (Double) agent.f_position.get(1).f_value;
			x2 = (Double) a.f_position.get(0).f_value;
			y2 = (Double) a.f_position.get(1).f_value;

			x = x2 - x1;
			y = y2 - y1;

			distance = Math.hypot(x, y);

			if (distance < closestDistance && a.f_type.f_value.equals(type) && !a.equals(agent)) {
				closestDistance = distance;
				closestAngle = Math.atan2(x, y);
				closestAgent = a;
			}
		}

		if (closestAgent != null) {
			input.add(closestDistance);
			input.add(closestAngle);
			input.add((Double) closestAgent.f_hp.f_value);
		} else {
			input.add(Double.NaN); // TODO make sure these values are correctly handled by their receivers
			input.add(Double.NaN);
			input.add(Double.NaN);
		}

		return input;
	}

	public static double[] normalize(double[] series) {
		double[] bound = new double[2];
		double[] norm = new double[series.length];

		bound[0] = Double.MAX_VALUE;
		bound[1] = Double.MIN_VALUE;

		for(int e = 0; e < series.length; e++) {
			bound[0] = Math.min(series[e], bound[0]); 
			bound[1] = Math.max(series[e], bound[1]); 
		}

		for(int e = 0; e < series.length; e++) {
//			System.err.print(series[e] + "|"); // DEBUG

			if(bound[0] != bound[1]) {
				norm[e] = (series[e]-bound[0]) / Math.abs(bound[1]-bound[0]);
			} else {
				norm[e] = 1.0;
			}

//			System.err.print(norm[e] + " "); // DEBUG
		}
//		System.err.println(); // DEBUG
		return norm; 
	}

	public static double[][] normalize(double[][] series) {
		double[][] norm = new double[series.length][];

		for(int e = 0; e < series.length; e++) {
			norm[e] = normalize(series[e]);
		}

		System.err.println(); // DEBUG
		return norm;
	}

	// TODO need array slicing and redimensioning

	public static double[] series_stat_mean(double[][] series) {
		double[] stat = new double[series.length];

		for (int t = 0; t < series.length; t++) {
			double mean = 0;

			for (int e = 0; e < series[t].length; e++) {
				mean += series[t][e];
			}

			mean = mean/series[t].length;

			stat[t] = mean;
		}

		return stat;
	}

	public static double[][] nested_arraylists_to_matrix(ArrayList<ArrayList<Double>> nls) {
		int t = 0;
		double[][] m = new double[nls.size()][];

		for (ArrayList<Double> l : nls) {
			int e = 0;
			m[t] = new double[l.size()];

			for (Double d : l) {
				m[t][e] = d;
				e++;
			}

			t++;
		}

		return m;
	}

	public static double[][] t(double[][] m) {
		double[][] tm = new double[m[0].length][m.length];
		for(int i = 0; i < m.length; i++) {
			for(int j = 0; j < m[0].length; j++) {
				tm[j][i] = m[i][j];
			}
		}

		return tm;
	}

	public static Double[] dD(double[] v) {
		Double[] Dv = new Double[v.length];
		for(int i = 0; i < v.length; i++) {
			Dv[i] = v[i];
			System.err.print(v[i] + " "); // DEBUG
		}
		System.err.println(); // DEBUG
		return Dv;
	}

	// Clustering functions
	// Clusters vectors of arbitrary length.
	public static class Clusterer implements Serializable {
		private static final long serialVersionUID = -1030381842508277184L;

		private TestFunction f_tf = null;
		private double[] f_cl = null;
		private double[][] f_ch = null;
		private int f_ch_lngth = 0;

		public Clusterer(TestFunction tf) {
			f_tf = tf;
		}
		public double[] get_cl() {assert(f_cl != null); return f_cl;}
		public double[][] get_ch() {assert(f_ch != null); return f_ch;}
		public int get_ch_lngth() {assert(f_ch_lngth != 0); return f_ch_lngth;}

		public void clusterer_init(double[] cl, double[][] ch, int ch_lngth, double[][] lst){
			cern.jet.random.engine.MersenneTwister rgen = new cern.jet.random.engine.MersenneTwister(new java.util.Date());
			cern.jet.random.Uniform urand = new cern.jet.random.Uniform(1, lst.length-1, rgen);
			int[] rl = cern.colt.GenericPermuting.permutation(urand.nextInt(), lst.length); 

			boolean found = false;
			f_ch_lngth = ch_lngth;

			for(int j = 0; j < lst.length; j++) {
				found = false;

				if(ch.length > 0) {
					for(int i = 0; i < ch.length; i++) {
						if(f_tf.cmp(lst[rl[j]], ch[i])) {
							cl[rl[j]] = i;
							found = true;
						}
					}
				}

				if(!found) {
					ch[f_ch_lngth] = lst[rl[j]];
					cl[rl[j]] = f_ch_lngth;
					f_ch_lngth++;
				}
			}

			f_cl = cl;
			f_ch = ch;
		}
	}

	public static interface TestFunction {
		public boolean cmp(double[] a, double[] b);
	}

	// TODO use a colt function class instead 
	public static class Hyp_TF implements TestFunction, Serializable {
		private static final long serialVersionUID = -1170337586489332391L;

		private double f_p;
		private int f_n;

		public Hyp_TF(double p, int n) {
			f_p = p;
			f_n = n;
		}

		public boolean cmp(double[] a, double[] b) {
			assert(a.length == b.length);

			double dst = dist(a, b);

			double sd = Math.sqrt(a.length); // TODO very bad approximation of population standard deviation 
			double rng = cern.jet.stat.Probability.normalInverse(.5+f_p/2) * sd / Math.sqrt(f_n);

			return rng > dst;
		}
	}

	static double max(double[] lst) {
		double mx = Double.MAX_VALUE;
		for(int i = 0; i < lst.length; i++) {
			mx = Math.max(mx, lst[i]);
		}
		return mx;
	}

	static double dist(double[] a, double[] b) {
		assert(a.length == b.length);
		double not_sqrt_hyp = 0;
		for(int i = 0; i < a.length; i++) {
			not_sqrt_hyp += Math.pow(a[i] - b[i], 2.0);
		}
		return Math.sqrt(not_sqrt_hyp);
	}

	public static double distMan(double[] a, double[] b) {
		assert(a.length == b.length);
		double total = 0.0;
		for (int i = 0; i < a.length; i++) {
			total += Math.abs(a[i] - b[i]);
		}
		return total;
	}

	public static void main(String args[]) throws IOException, ClassNotFoundException {
		double[] mu = {1.0, 20.0, 100.0, 1.0, 1.0, 3.0, 36.0, 20.0, 3.0, 3.0};
		int sets = mu.length;
		int smpls = 10;
		int filds = 5;
		double[][] set = new double[sets][filds];

		cern.jet.random.engine.MersenneTwister rgen = new cern.jet.random.engine.MersenneTwister(new java.util.Date());

		for(int currSet = 0; currSet < sets; currSet++){ 
			cern.jet.random.Normal rnorm = new Normal(mu[currSet], 1, rgen);
			
			for(int i = 0; i < smpls; i++) {
				for(int j = 0; j < set[currSet].length; j++) {
					set[currSet][j] += rnorm.nextDouble();
				}
			}
			
			for(int j = 0; j < set[currSet].length; j++) {
				set[currSet][j] /= smpls;
			}
		}

		set = t(normalize(t(set)));
		
		Clusterer clusterer = new Clusterer(new Hyp_TF(0.05, smpls));
		
		int max_clsss = sets;
		double[] cl = new double[max_clsss];
		for(int i = 0; i < max_clsss; i++) {
			cl[i] = -1;
		}
		double[][] ch = new double[max_clsss][filds];
		
		clusterer.clusterer_init(cl, ch, 0, set);
		
		System.out.println(clusterer.get_ch_lngth());
		
		ModelState state = new ModelState();
		
		System.out.println(deepClone(new ExternalInputAgent(state)));
		// System.out.println(deepClone(new ExternalInputAgent(new ModelState())));
		// System.out.println(deepClone("hi"));
	}
	
	public static int[] intMap(Collection<Object> c, ObjectFunction f) {
		int[] result = new int[c.size()];
		int i = 0; for(Object o : c) {
			result[i++] = (Integer) f.apply(o);
		}
		return result;
	}
	
	// TODO create an object based mapping utility, with means of converting to other types and data structures
	
	// PRECONDITION: object to be cloned must be serializable
	public static Object deepClone(Object o) throws IOException, ClassNotFoundException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(o);
		oos.flush();

		byte[] out = baos.toByteArray();
		ByteArrayInputStream bais = new ByteArrayInputStream(out);
		ObjectInputStream ois = new ObjectInputStream(bais);
		Object result = ois.readObject();
		
		return result;
	}
	
	public static boolean compareListsAll(List<Object> a, List<Object> b) {
		assert(a.size() == b.size());
		for(int i = 0; i < a.size(); i++)
			if(!a.get(i).equals(b.get(i))) return false;
		return true;
	}
	
	@SuppressWarnings("rawtypes")
	public static boolean compareListsAny(List a, List b) {
		assert(a.size() == b.size());
		for(int i = 0; i < a.size(); i++)
			if(a.get(i).equals(b.get(i))) return true;
		return false;
	}
	
	public static boolean compareDoubleArrayAny(double[] a, double[] b) {
		assert(a.length == b.length);
		for(int i = 0; i < a.length; i++)
			if(a[i] == b[i]) return true;
		return false;
	}
	
	public static Box regexGet(String regexp, TreeMap<String, Box> map) {
		for(Entry<String, Box> e : map.entrySet())
			if(Pattern.matches(regexp, e.getKey())) return e.getValue();
		return null;
	}
}
